package com.duosecurity.x_ray; import android.content.Context; import android.os.AsyncTask; import android.util.JsonReader; import android.util.Log; import com.duosecurity.duokit.crypto.Crypto; import org.thoughtcrime.ssl.pinning.util.PinningHelper; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.security.PublicKey; import java.security.Signature; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLHandshakeException; public class XrayCheckTask extends AsyncTask<Void, Void, XrayUpdater.CheckResult> { private final static String TAG = XrayCheckTask.class.getSimpleName(); private Context context = null; private Crypto crypto = null; private TaskListener callback = null; public interface TaskListener { void onFinished(XrayUpdater.CheckResult checkResult); } public XrayCheckTask (Context ctx, TaskListener taskListener) { context = ctx; crypto = Crypto.getInstance(); callback = taskListener; } @Override protected XrayUpdater.CheckResult doInBackground (Void... v) { HttpsURLConnection urlConnection = null; InputStream inputStream = null; XrayUpdater.CheckResult result = XrayUpdater.CheckResult.UP_TO_DATE; Log.d(TAG, "Attempting to fetch manifest..."); try { // issue a GET request to determine the latest available apk version URL url = new URL(XrayUpdater.VERSION_URL); urlConnection = PinningHelper.getPinnedHttpsURLConnection( context, XrayUpdater.CERT_PINS, url ); urlConnection.setConnectTimeout(XrayUpdater.CONNECTION_TIMEOUT); urlConnection.setReadTimeout(XrayUpdater.READ_TIMEOUT); urlConnection.setRequestMethod("GET"); urlConnection.setDoInput(true); urlConnection.connect(); int responseCode = urlConnection.getResponseCode(); int apkVersion = -1; String apkName = null; String apkChecksum = null; // read the results into a byte array stream inputStream = new BufferedInputStream(urlConnection.getInputStream()); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); if (responseCode != HttpURLConnection.HTTP_OK) { Log.d(TAG, "Error fetching app version, HTTP request returned: " + responseCode); } else if (!XrayUpdater.writeToOutputStream(inputStream, byteStream)) { Log.d(TAG, "Error fetching app version, invalid input stream"); } else { // request looks okay, let's verify response signature Signature ecdsaSignature = Signature.getInstance( XrayUpdater.ECDSA_ALGORITHM, XrayUpdater.ECDSA_PROVIDER ); PublicKey extPubKey = crypto.readPublicKey(XrayUpdater.SERVER_PUB_KEY); ecdsaSignature.initVerify(extPubKey); ecdsaSignature.update(byteStream.toByteArray()); String signature = urlConnection.getHeaderField("Xray-Signature"); byte[] signature_bytes = crypto.base64Decode(signature); if (!ecdsaSignature.verify(signature_bytes)) { Log.d(TAG, "Invalid signature"); } else { Log.d(TAG, "Signature valid. Reading JSON response..."); // signature is valid, so read version and filename from JSON response inputStream = new ByteArrayInputStream(byteStream.toByteArray()); JsonReader reader = new JsonReader(new InputStreamReader(inputStream)); reader.beginObject(); while (reader.hasNext()) { String key = reader.nextName(); if (key.equals("apkVersion")) { apkVersion = Integer.parseInt(reader.nextString()); } else if (key.equals("apkName")) { apkName = reader.nextString(); } else if (key.equals("apkChecksum")) { apkChecksum = reader.nextString(); } else { reader.skipValue(); } } reader.endObject(); } } if (apkVersion < 0 || apkName == null || apkChecksum == null) { Log.d(TAG, "Error fetching app version, JSON response missing fields"); } else if (apkVersion == BuildConfig.VERSION_CODE) { Log.d(TAG, "Already up to date"); } else { // out of date XrayUpdater.setSharedPreference("apkName", apkName); XrayUpdater.setSharedPreference("apkChecksum", apkChecksum); result = XrayUpdater.CheckResult.OUT_OF_DATE; } } catch (MalformedURLException e) { Log.d(TAG, "Found malformed URL when trying to update"); } catch (SocketTimeoutException e) { Log.d(TAG, "Socket timed out when trying to update: " + e.toString()); } catch (SSLHandshakeException e) { Log.d(TAG, "Failed SSL Handshake when trying to update: " + e.toString()); result = XrayUpdater.CheckResult.SSL_ERROR; } catch (IOException e) { Log.d(TAG, "Found IO exception when trying to update: " + e.toString()); } catch (Exception e) { Log.d(TAG, "Received error when trying to update: " + e.toString()); } finally { Log.d(TAG, "Cleaning up check task..."); // close the GET connection if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { Log.d(TAG, "Found IO exception when trying to close inputstream: " + e.toString()); } } if (urlConnection != null) { urlConnection.disconnect(); } Log.d(TAG, "Exiting check task"); } return result; } @Override protected void onPostExecute(XrayUpdater.CheckResult checkResult) { super.onPostExecute(checkResult); if (callback != null) { callback.onFinished(checkResult); } } }